﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Comdiv.QWeb.Binding;
using Comdiv.QWeb.Factory;
using Comdiv.QWeb.Serialization.Attributes;
using Comdiv.QWeb.Utils;

namespace Comdiv.QWeb {
	/// <summary>
	/// Describes how to bind input parameters to action during initialization, used by IActionBinder
	/// </summary>
	[Serialize]
	public class BindAttribute : Attribute {
		private string _errorMessage;
		private string _parameterName;
		private Type _targettype;

		public BindAttribute() {
		}

		public BindAttribute(object def) {
			Default = def;
		}

		public bool IsLargeText { get; set; }
		public bool IsComplexString { get; set; }
		public bool IsColor { get; set; }

		[IgnoreSerialize] public bool IsString {
			get { return typeof (string) == TargetType; }
		}

		[IgnoreSerialize] public bool IsBool {
			get { return typeof (bool) == TargetType; }
		}

		[IgnoreSerialize] public bool IsDate {
			get { return typeof (DateTime) == TargetType; }
		}

		[IgnoreSerialize] public bool IsEnum {
			get { return TargetType.IsEnum; }
		}

		[Serialize] public object Default { get; set; }
		public MemberInfo Member { get; set; }

		public bool RequireValidation {
			get { return Required || (null != Constraint && 0 != Constraint.Length); }
		}

		public bool LowerCase { get; set; }
		public bool UpperCase { get; set; }

		public string Name {
			get {
				if (string.IsNullOrEmpty(_parameterName)) return Member.Name;
				return _parameterName;
			}
			set { _parameterName = value; }
		}

		public string TypeName {
			get { return TargetType.FullName; }
		}

		public Type TargetType {
			get {
				if (null == _targettype) {
					if (Member is FieldInfo) {
						_targettype = ((FieldInfo) Member).FieldType;
					}
					if (Member is PropertyInfo) {
						_targettype = ((PropertyInfo) Member).PropertyType;
					}
				}
				return _targettype;
			}
		}

		public bool Required { get; set; }

		public string ValidatePattern { get; set; }

		public string ErrorMessage {
			get {
				if (_errorMessage.noContent()) {
					_errorMessage = string.Format("Parameter '{0}' :", Name);
					if (Required) {
						_errorMessage += " is required;";
					}
					if (null != Constraint && 0 != Constraint.Length) {
						_errorMessage += " is constrainted to " + Constraint.concat(", ") + ";";
					}
				}
				return _errorMessage;
			}
			set { _errorMessage = value; }
		}

		public object[] Constraint { get; set; }

		public string Help { get; set; }

		public BindAttribute GetThreadSafeCopy(MemberInfo i = null) {
			i = i ?? Member;
			var copy = MemberwiseClone() as BindAttribute;
			copy.Member = i;
			return copy;
		}

		public virtual void SetConverted(object action, string val, QWebContext context) {
			var realval = BindTypeConverter.DoConvert(val, TargetType, Default);
			if (TargetType == typeof (string)) {
				var s = (string) realval;
				if (null != s) {
					if (LowerCase) {
						realval = s.ToLower();
					}
					if (UpperCase) {
						realval = s.ToUpper();
					}
				}
			}
			if (Member is FieldInfo) {
				((FieldInfo) Member).SetValue(action, realval);
			}
			else if (Member is PropertyInfo) {
				((PropertyInfo) Member).SetValue(action, realval, null);
			}
		}

		public void SetDirectly(object action, object val) {
			if (Member is FieldInfo) {
				((FieldInfo) Member).SetValue(action, val);
			}
			else if (Member is PropertyInfo) {
				((PropertyInfo) Member).SetValue(action, val, null);
			}
		}

		public object GetCurrent(object action) {
			if (Member is FieldInfo) {
				return ((FieldInfo) Member).GetValue(action);
			}
			else if (Member is PropertyInfo) {
				return ((PropertyInfo) Member).GetValue(action, null);
			}
			return null;
		}

		private void validateBinding(QWebContext context, ActionDescriptor action) {
			if (!RequireValidation) {
				return;
			}
			var val = GetCurrent(action.Action);
			var isValid = getIsValid(action, context, val);
			if (!isValid) {
				throw new BindException(this, context, action, val);
			}
		}

		protected virtual bool getIsValid(ActionDescriptor action, QWebContext context, object val) {
			var isValid = true;
			if (Required) {
				if (TargetType == typeof (string)) {
					if (string.IsNullOrEmpty(val.ToString())) {
						isValid = false;
					}
					else if (null != ValidatePattern) {
						var pattern = ValidatePattern.Replace("\\\\", "\\");
						if (!new Regex(pattern).IsMatch(val.ToString())) {
							isValid = false;
						}
					}
				}
				else if (TargetType.IsValueType) {
					if (!context.Parameters.ContainsKey(Name)) {
						isValid = false;
					}
				}
				else if (TargetType == typeof (XElement)) {
					if (null == val || ((XElement) val).Name.LocalName == "empty") {
						isValid = false;
					}
				}
				else {
					if (null == val) {
						isValid = false;
					}
				}
			}
			if (isValid && (null != Constraint && 0 != Constraint.Length) && (!string.IsNullOrEmpty(val.toStr()))) {
				if (-1 == Array.IndexOf(Constraint, val)) isValid = false;
			}
			return isValid;
		}

		private void setupValue(QWebContext context, ActionDescriptor action) {
			var paramname = Name;
			if (TargetType == typeof (XElement)) {
				var val = context.getx(Name);
				SetDirectly(action.Action, val);
			}
			else if (TargetType.IsArray) {
				var val = context.getarray(TargetType.GetElementType(), paramname);
				SetDirectly(action.Action, val);
			}
			else if (typeof (IDictionary<string, string>) == TargetType) {
				var current = (IDictionary<string, string>) GetCurrent(action.Action);
				if (null == current) {
					current = new Dictionary<string, string>();
					SetDirectly(action.Action, current);
				}

				IDictionary<string, string> val;
				if (IsComplexString) {
					var str = context.get(paramname);
					val = ComplexStringHelper.Parse(str);
				}
				else {
					val = context.getdict(paramname);
				}
				foreach (var p in val) {
					current[p.Key] = p.Value;
				}
			}
			else {
				var val = context.get(paramname);
				SetConverted(action.Action, val, context);
			}
		}

		public void Bind(ActionDescriptor action, QWebContext context) {
			setupValue(context, action);
			validateBinding(context, action);
		}
	}
}